package pab.core.service;

import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;

import pab.core.account.Account;
import pab.core.account.Accounts;
import pab.core.exception.AccountException;
import pab.core.exception.TransactionException;
import pab.core.transaction.Currency;
import pab.core.transaction.ProjectedInterval;
import pab.core.transaction.Transaction;
import pab.core.transaction.TransactionType;

public class AccountServiceImpl implements AccountService {

	private Accounts accounts;
	private String accountsFilePath;
	private Marshaller marshaller;

	private ExchangeRate exchanger;
	
	private List<Transaction> latestProjectedTrans = new ArrayList<Transaction>();

	public AccountServiceImpl(String accountsFilePath, ExchangeRate exchangeRate) throws AccountException {
		this.accountsFilePath = accountsFilePath;
		exchanger = exchangeRate;
		try {
			JAXBContext context;
			context = JAXBContext.newInstance("pab.core.account:pab.core.transaction");

			Unmarshaller unmarshaller = context.createUnmarshaller();
			accounts = (Accounts) unmarshaller.unmarshal(new FileReader(accountsFilePath));
			for (Account account : accounts.getAccount()) {
				for (Transaction transaction : account.getTransaction()) {
					transaction.setAccount(account);
					if (transaction.isProjected() && transaction.isLatest()) {
						latestProjectedTrans.add(transaction);
					}
				}
			}

			marshaller = context.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			
			generateProjectedTransaction();
		} catch (Exception e) {
			throw new AccountException(e);
		}
		
		new Timer().scheduleAtFixedRate(new TimerTask() {
	        public void run() {
	        	generateProjectedTransaction();
	        }
	    }, 24 * 3600 * 1000, 24 * 3600 * 1000);
		
		new Timer().scheduleAtFixedRate(new TimerTask() {
	        public void run() {
	        	save();
	        }
	    }, 5 * 60 * 1000, 5 * 60 * 1000);
	}

	private void generateProjectedTransaction() {
		List<Transaction> tempList = new ArrayList<Transaction>(latestProjectedTrans);
        for (Transaction transaction : tempList) {
        	XMLGregorianCalendar dueDate = (XMLGregorianCalendar) transaction.getDate().clone();
        	switch (transaction.getInterval()) {
			case MONTH:
				dueDate.add(TimeConstant.DURATION_MONTH);
				break;
			case QUARTER:
				dueDate.add(TimeConstant.DURATION_QUATER);
				break;
			case HALF_YEAR:
				dueDate.add(TimeConstant.DURATION_HALF_YEAR);
				break;
			case YEAR:
				dueDate.add(TimeConstant.DURATION_YEAR);
				break;
			}
        	
        	if (isDueDate(dueDate)) {
        		try {
                    latestProjectedTrans.remove(transaction);
        		    makePriliminaryTransaction(transaction.getAccount(), transaction.getType(), transaction.getAmount(),
							transaction.getCurrency(), transaction.getDesc(), TimeConstant.rightNow(), transaction.getInterval());

        			transaction.setLatest(false);
				} catch (TransactionException e) {
					throw new RuntimeException(e);
				}
        	}
        }
	}
	private boolean isDueDate(XMLGregorianCalendar dueDate) {
		XMLGregorianCalendar today = TimeConstant.rightNow();
		if (today.getDay() == dueDate.getDay() && today.getMonth() == dueDate.getMonth() && today.getYear() == dueDate.getYear()) {
			return true;
		}
		return false;
	}
	public synchronized void save() {
		try {
			marshaller.marshal(accounts, new FileWriter(accountsFilePath));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public void clearTransation(Transaction transaction) throws TransactionException {

		Account account = transaction.getAccount();
		double targetAmount = transaction.getAmount() * transaction.getExchangeRate();
		switch (transaction.getType()) {
		case CREDIT:
			account.setCurrentBalance(account.getCurrentBalance() + targetAmount);
			break;

		case DEBIT:
			account.setCurrentBalance(account.getCurrentBalance() - targetAmount);
			break;
		}
		
		transaction.setCleared(true);

	}
	
	/**
	 * if the transaction has been cleared, just mark it as deleted
	 * otherwise we restore the available balance of associative account and remove the transaction
	 */
	public void deleteTransation(Transaction transaction) throws TransactionException {
		
		if (transaction.isCleared()) {
		    //transaction.setDeleted(true);
		    throw new TransactionException("Cannot delete an already-cleared transaction");
		} else {
			Account account = transaction.getAccount();
			
			double targetAmount = transaction.getAmount() * transaction.getExchangeRate();
			switch (transaction.getType()) {
			case CREDIT:
				account.setAvailableBalance(account.getAvailableBalance() - targetAmount);
				break;

			case DEBIT:
				account.setAvailableBalance(account.getAvailableBalance() + targetAmount);
				break;
			}
			//account.getTransaction().remove(transaction);
			transaction.setDeleted(true);
			if (transaction.isProjected() && transaction.isLatest()) {
			    latestProjectedTrans.remove(transaction);
			}
		}
	}

	public void setProjected(Transaction transaction, boolean projected) throws TransactionException {

		if (projected && transaction.isProjected()) {
			throw new TransactionException("This transaction has been already projected");
		}
		transaction.setProjected(projected);
		transaction.setLatest(projected);
		if (projected) {
		latestProjectedTrans.add(transaction);
		} else {
			latestProjectedTrans.remove(transaction);
		}
	}

	public Account openAccount(String number, String name, Currency currency,
			double balance) throws AccountException {

		for (Account account : accounts.getAccount()) {
			if (account.getNumber().equals(number)) throw new AccountException("This account number has been existed");
		}
		Account account = new Account(number, name, currency, balance);
		accounts.getAccount().add(account);

		return account;

	}
	
	public void closeAccount(Account account) throws AccountException {
		accounts.getAccount().remove(account);
	}

	/**
	 * does nothing, all account has been balanced incrementally during processing of transaction (create a preliminary transaction or clear a transation
	 */
	public void balanceAccount(Account account) throws AccountException {


	}

	public Transaction makePriliminaryTransaction(Account account, TransactionType type, double amount,
			Currency currency, String desc, XMLGregorianCalendar date) throws TransactionException {

		Transaction transaction = new Transaction(UUID.randomUUID().toString(), account, type, amount, currency, desc, date);
		account.getTransaction().add(transaction);

		double rate = exchanger.exchangeRate(currency, account.getCurrency());
		
		transaction.setExchangeRate(rate);
		
		double targetAmount = amount * rate;
		switch (type) {
		case CREDIT:
			account.setAvailableBalance(account.getAvailableBalance() + targetAmount);
			break;

		case DEBIT:
			account.setAvailableBalance(account.getAvailableBalance() - targetAmount);
			break;
		}
		return transaction;
	}

	public Transaction makePriliminaryTransaction(Account account, TransactionType type, double amount,
			Currency currency, String desc, XMLGregorianCalendar date, ProjectedInterval interval) throws TransactionException {
		Transaction transaction = makePriliminaryTransaction(account, type, amount, currency, desc, date);
		
		transaction.setProjected(true);
		transaction.setInterval(interval);
		transaction.setLatest(true);
		
		latestProjectedTrans.add(transaction);
		return null;
	}

	public List<Account> listAccounts() {
		return accounts.getAccount();
	}
	
	public Account getAccount(String number) {
	    for (Account account : listAccounts()) {
            if(account.getNumber().equals(number)) {
                return account;
            }
        }
	    return null;
	}

	
	public List<Transaction> listTransactions() throws AccountException {
	    ArrayList<Transaction> transactions = new ArrayList<Transaction>();
	    for (Account account : listAccounts()) {
            transactions.addAll(listTransactions(account));
        }
	    
	    return Collections.unmodifiableList(transactions);
	}
	
	public List<Transaction> listTransactions(Account account) throws AccountException {

		return account.getTransaction();
	}

	public List<Transaction> listTransactions(Account account, boolean preliminary) throws AccountException {

		return listTransactions(account, new Condition("cleared", preliminary));
	}

	public List<Transaction> listTransactions(Account account, Condition condition) throws AccountException {

		List<Transaction> transactions = new ArrayList<Transaction>();

		for (Transaction transaction : account.getTransaction()) {

			if (condition == null || match(condition, transaction)) {
				transactions.add(transaction);
			}
		}

		return transactions;
	}
	
	public List<Transaction> listTransactions(Account account, XMLGregorianCalendar fromDate,
			XMLGregorianCalendar toDate) throws AccountException {

		return listTransactions(account, null, fromDate, toDate);

	}



	public List<Transaction> listTransactions(Account account,
			boolean preliminary, XMLGregorianCalendar fromDate, XMLGregorianCalendar toDate)
			throws AccountException {

		Condition condition = new Condition("cleared", false); 

		return listTransactions(account, condition, fromDate, toDate);
	}

	public List<Transaction> listTransactions(Account account,
			Condition condition, XMLGregorianCalendar fromDate, XMLGregorianCalendar toDate)
			throws AccountException {

		if (compareOnlyYMD(fromDate, toDate) == DatatypeConstants.GREATER) throw new AccountException("fromDate cannot be greater than toDate");

		List<Transaction> transactions = new ArrayList<Transaction>();

		for (Transaction transaction : account.getTransaction()) {
			int compareFrom = compareOnlyYMD(transaction.getDate(), fromDate);
			int compareTo = compareOnlyYMD(transaction.getDate(), toDate);

			if ((compareFrom == DatatypeConstants.GREATER || compareFrom == DatatypeConstants.EQUAL) &&
					(compareTo == DatatypeConstants.LESSER || compareTo == DatatypeConstants.EQUAL)) {

				if (condition == null || match(condition, transaction)) transactions.add(transaction);
			}
		}

		return transactions;
	}
	
	public List<Transaction> listLatestProjectedTransaction() {
		return Collections.unmodifiableList(latestProjectedTrans);
	}
	
	private int compareOnlyYMD(XMLGregorianCalendar date1, XMLGregorianCalendar date2) {
		if (date1.getYear() > date2.getYear()) return DatatypeConstants.GREATER;
		if (date1.getYear() < date2.getYear())  return DatatypeConstants.LESSER;
		
		if (date1.getMonth() > date2.getMonth()) return DatatypeConstants.GREATER;
		if (date1.getMonth() < date2.getMonth())  return DatatypeConstants.LESSER;
		
		if (date1.getDay() > date2.getDay()) return DatatypeConstants.GREATER;
		if (date1.getDay() < date2.getDay())  return DatatypeConstants.LESSER;
		
		return DatatypeConstants.EQUAL;
		
	}
	
	private boolean match(Condition condition, Transaction transation) throws AccountException {
		boolean equal;
		try {
			equal = condition.value.equals(condition.method.invoke(transation, new Object[0]));
		} catch (Exception e) {
			throw new AccountException(e);
		}

		if (condition.nextCondition == null) return equal;
		if (condition.op.equals(Condition.LogicOperator.AND)) {
			return equal && match(condition.nextCondition, transation); 
		} else {
			return equal || match(condition.nextCondition, transation); 
		}

	}
	
	public ExchangeRate getExchangeService() {
		return exchanger;
	}
}
